 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.runtime;

 import java.io.File ;

 /**
  * The standard implementation of the <code>IPath</code> interface.
  * Paths are always maintained in canonicalized form. That is, parent
  * references (i.e., <code>../../</code>) and duplicate separators are
  * resolved. For example,
  * <pre> new Path("/a/b").append("../foo/bar")</pre>
  * will yield the path
  * <pre> /a/foo/bar</pre>
  * <p>
  * This class can be used without OSGi running.
  * </p><p>
  * This class is not intended to be subclassed by clients but
  * may be instantiated.
  * </p>
  * @see IPath
  */
 public class Path implements IPath, Cloneable {
     /** masks for separator values */
     private static final int HAS_LEADING = 1;
     private static final int IS_UNC = 2;
     private static final int HAS_TRAILING = 4;

     private static final int ALL_SEPARATORS = HAS_LEADING | IS_UNC | HAS_TRAILING;

     /** Constant empty string value. */
     private static final String EMPTY_STRING = ""; //$NON-NLS-1$

     /** Constant value indicating no segments */
     private static final String [] NO_SEGMENTS = new String [0];

     /** Constant value containing the empty path with no device. */
     public static final Path EMPTY = new Path(EMPTY_STRING);

     /** Mask for all bits that are involved in the hash code */
     private static final int HASH_MASK = ~HAS_TRAILING;


     /** Constant root path string (<code>"/"</code>). */
     private static final String ROOT_STRING = "/"; //$NON-NLS-1$

     /** Constant value containing the root path with no device. */
     public static final Path ROOT = new Path(ROOT_STRING);
     
     /** Constant value indicating if the current platform is Windows */
     private static final boolean WINDOWS = java.io.File.separatorChar == '\\';

     /** The device id string. May be null if there is no device. */
     private String device = null;

     //Private implementation note: the segments and separators
 //arrays are never modified, so that they can be shared between
 //path instances

     /** The path segments */
     private String [] segments;

     /** flags indicating separators (has leading, is UNC, has trailing) */
     private int separators;

     /**
      * Constructs a new path from the given string path.
      * The string path must represent a valid file system path
      * on the local file system.
      * The path is canonicalized and double slashes are removed
      * except at the beginning. (to handle UNC paths). All forward
      * slashes ('/') are treated as segment delimiters, and any
      * segment and device delimiters for the local file system are
      * also respected.
      *
      * @param pathString the portable string path
      * @see IPath#toPortableString()
      * @since 3.1
      */
     public static IPath fromOSString(String pathString) {
         return new Path(pathString);
     }
     
     /**
      * Constructs a new path from the given path string.
      * The path string must have been produced by a previous
      * call to <code>IPath.toPortableString</code>.
      *
      * @param pathString the portable path string
      * @see IPath#toPortableString()
      * @since 3.1
      */
     public static IPath fromPortableString(String pathString) {
         int firstMatch = pathString.indexOf(DEVICE_SEPARATOR) +1;
         //no extra work required if no device characters
 if (firstMatch <= 0)
             return new Path().initialize(null, pathString);
         //if we find a single colon, then the path has a device
 String devicePart = null;
         int pathLength = pathString.length();
         if (firstMatch == pathLength || pathString.charAt(firstMatch) != DEVICE_SEPARATOR) {
             devicePart = pathString.substring(0, firstMatch);
             pathString = pathString.substring(firstMatch, pathLength);
         }
         //optimize for no colon literals
 if (pathString.indexOf(DEVICE_SEPARATOR) == -1)
             return new Path().initialize(devicePart, pathString);
         //contract colon literals
 char[] chars = pathString.toCharArray();
         int readOffset = 0, writeOffset = 0, length = chars.length;
         while (readOffset < length) {
             if (chars[readOffset] == DEVICE_SEPARATOR)
                 if (++readOffset >= length)
                     break;
             chars[writeOffset++] = chars[readOffset++];
         }
         return new Path().initialize(devicePart, new String (chars, 0, writeOffset));
     }
     
     /* (Intentionally not included in javadoc)
      * Private constructor.
      */
     private Path() {
         // not allowed
 }

     /**
      * Constructs a new path from the given string path.
      * The string path must represent a valid file system path
      * on the local file system.
      * The path is canonicalized and double slashes are removed
      * except at the beginning. (to handle UNC paths). All forward
      * slashes ('/') are treated as segment delimiters, and any
      * segment and device delimiters for the local file system are
      * also respected (such as colon (':') and backslash ('\') on some file systems).
      *
      * @param fullPath the string path
      * @see #isValidPath(String)
      */
     public Path(String fullPath) {
         String devicePart = null;
         if (WINDOWS) {
             //convert backslash to forward slash
 fullPath = fullPath.indexOf('\\') == -1 ? fullPath : fullPath.replace('\\', SEPARATOR);
             //extract device
 int i = fullPath.indexOf(DEVICE_SEPARATOR);
             if (i != -1) {
                 //remove leading slash from device part to handle output of URL.getFile()
 int start = fullPath.charAt(0) == SEPARATOR ? 1 : 0;
                 devicePart = fullPath.substring(start, i + 1);
                 fullPath = fullPath.substring(i + 1, fullPath.length());
             }
         }
         initialize(devicePart, fullPath);
     }

     /**
      * Constructs a new path from the given device id and string path.
      * The given string path must be valid.
      * The path is canonicalized and double slashes are removed except
      * at the beginning (to handle UNC paths). All forward
      * slashes ('/') are treated as segment delimiters, and any
      * segment delimiters for the local file system are
      * also respected (such as backslash ('\') on some file systems).
      *
      * @param device the device id
      * @param path the string path
      * @see #isValidPath(String)
      * @see #setDevice(String)
      */
     public Path(String device, String path) {
         if (WINDOWS) {
             //convert backslash to forward slash
 path = path.indexOf('\\') == -1 ? path : path.replace('\\', SEPARATOR);
         }
         initialize(device, path);
     }

     /* (Intentionally not included in javadoc)
      * Private constructor.
      */
     private Path(String device, String [] segments, int _separators) {
         // no segment validations are done for performance reasons
 this.segments = segments;
         this.device = device;
         //hash code is cached in all but the bottom three bits of the separators field
 this.separators = (computeHashCode() << 3) | (_separators & ALL_SEPARATORS);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#addFileExtension
      */
     public IPath addFileExtension(String extension) {
         if (isRoot() || isEmpty() || hasTrailingSeparator())
             return this;
         int len = segments.length;
         String [] newSegments = new String [len];
         System.arraycopy(segments, 0, newSegments, 0, len - 1);
         newSegments[len - 1] = segments[len - 1] + '.' + extension;
         return new Path(device, newSegments, separators);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#addTrailingSeparator
      */
     public IPath addTrailingSeparator() {
         if (hasTrailingSeparator() || isRoot()) {
             return this;
         }
         //XXX workaround, see 1GIGQ9V
 if (isEmpty()) {
             return new Path(device, segments, HAS_LEADING);
         }
         return new Path(device, segments, separators | HAS_TRAILING);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#append(IPath)
      */
     public IPath append(IPath tail) {
         //optimize some easy cases
 if (tail == null || tail.segmentCount() == 0)
             return this;
         //these call chains look expensive, but in most cases they are no-ops
 if (this.isEmpty())
             return tail.setDevice(device).makeRelative().makeUNC(isUNC());
         if (this.isRoot())
             return tail.setDevice(device).makeAbsolute().makeUNC(isUNC());

         //concatenate the two segment arrays
 int myLen = segments.length;
         int tailLen = tail.segmentCount();
         String [] newSegments = new String [myLen + tailLen];
         System.arraycopy(segments, 0, newSegments, 0, myLen);
         for (int i = 0; i < tailLen; i++) {
             newSegments[myLen + i] = tail.segment(i);
         }
         //use my leading separators and the tail's trailing separator
 Path result = new Path(device, newSegments, (separators & (HAS_LEADING | IS_UNC)) | (tail.hasTrailingSeparator() ? HAS_TRAILING : 0));
         String tailFirstSegment = newSegments[myLen];
         if (tailFirstSegment.equals("..") || tailFirstSegment.equals(".")) { //$NON-NLS-1$ //$NON-NLS-2$
 result.canonicalize();
         }
         return result;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#append(java.lang.String)
      */
     public IPath append(String tail) {
         //optimize addition of a single segment
 if (tail.indexOf(SEPARATOR) == -1 && tail.indexOf("\\") == -1 && tail.indexOf(DEVICE_SEPARATOR) == -1) { //$NON-NLS-1$
 int tailLength = tail.length();
             if (tailLength < 3) {
                 //some special cases
 if (tailLength == 0 || ".".equals(tail)) { //$NON-NLS-1$
 return this;
                 }
                 if ("..".equals(tail)) //$NON-NLS-1$
 return removeLastSegments(1);
             }
             //just add the segment
 int myLen = segments.length;
             String [] newSegments = new String [myLen + 1];
             System.arraycopy(segments, 0, newSegments, 0, myLen);
             newSegments[myLen] = tail;
             return new Path(device, newSegments, separators & ~HAS_TRAILING);
         }
         //go with easy implementation
 return append(new Path(tail));
     }

     /**
      * Destructively converts this path to its canonical form.
      * <p>
      * In its canonical form, a path does not have any
      * "." segments, and parent references ("..") are collapsed
      * where possible.
      * </p>
      * @return true if the path was modified, and false otherwise.
      */
     private boolean canonicalize() {
         //look for segments that need canonicalizing
 for (int i = 0, max = segments.length; i < max; i++) {
             String segment = segments[i];
             if (segment.charAt(0) == '.' && (segment.equals("..") || segment.equals("."))) { //$NON-NLS-1$ //$NON-NLS-2$
 //path needs to be canonicalized
 collapseParentReferences();
                 //paths of length 0 have no trailing separator
 if (segments.length == 0)
                     separators &= (HAS_LEADING | IS_UNC);
                 //recompute hash because canonicalize affects hash
 separators = (separators & ALL_SEPARATORS) | (computeHashCode() << 3);
                 return true;
             }
         }
         return false;
     }

     /* (Intentionally not included in javadoc)
      * Clones this object.
      */
     public Object clone() {
         try {
             return super.clone();
         } catch (CloneNotSupportedException e) {
             return null;
         }
     }

     /**
      * Destructively removes all occurrences of ".." segments from this path.
      */
     private void collapseParentReferences() {
         int segmentCount = segments.length;
         String [] stack = new String [segmentCount];
         int stackPointer = 0;
         for (int i = 0; i < segmentCount; i++) {
             String segment = segments[i];
             if (segment.equals("..")) { //$NON-NLS-1$
 if (stackPointer == 0) {
                     // if the stack is empty we are going out of our scope
 // so we need to accumulate segments. But only if the original
 // path is relative. If it is absolute then we can't go any higher than
 // root so simply toss the .. references.
 if (!isAbsolute())
                         stack[stackPointer++] = segment; //stack push
 } else {
                     // if the top is '..' then we are accumulating segments so don't pop
 if ("..".equals(stack[stackPointer - 1])) //$NON-NLS-1$
 stack[stackPointer++] = ".."; //$NON-NLS-1$
 else
                         stackPointer--;
                     //stack pop
 }
                 //collapse current references
 } else if (!segment.equals(".") || segmentCount == 1) //$NON-NLS-1$
 stack[stackPointer++] = segment; //stack push
 }
         //if the number of segments hasn't changed, then no modification needed
 if (stackPointer == segmentCount)
             return;
         //build the new segment array backwards by popping the stack
 String [] newSegments = new String [stackPointer];
         System.arraycopy(stack, 0, newSegments, 0, stackPointer);
         this.segments = newSegments;
     }

     /**
      * Removes duplicate slashes from the given path, with the exception
      * of leading double slash which represents a UNC path.
      */
     private String collapseSlashes(String path) {
         int length = path.length();
         // if the path is only 0, 1 or 2 chars long then it could not possibly have illegal
 // duplicate slashes.
 if (length < 3)
             return path;
         // check for an occurrence of // in the path. Start at index 1 to ensure we skip leading UNC //
 // If there are no // then there is nothing to collapse so just return.
 if (path.indexOf("//", 1) == -1) //$NON-NLS-1$
 return path;
         // We found an occurrence of // in the path so do the slow collapse.
 char[] result = new char[path.length()];
         int count = 0;
         boolean hasPrevious = false;
         char[] characters = path.toCharArray();
         for (int index = 0; index < characters.length; index++) {
             char c = characters[index];
             if (c == SEPARATOR) {
                 if (hasPrevious) {
                     // skip double slashes, except for beginning of UNC.
 // note that a UNC path can't have a device.
 if (device == null && index == 1) {
                         result[count] = c;
                         count++;
                     }
                 } else {
                     hasPrevious = true;
                     result[count] = c;
                     count++;
                 }
             } else {
                 hasPrevious = false;
                 result[count] = c;
                 count++;
             }
         }
         return new String (result, 0, count);
     }

     /* (Intentionally not included in javadoc)
      * Computes the hash code for this object.
      */
     private int computeHashCode() {
         int hash = device == null ? 17 : device.hashCode();
         int segmentCount = segments.length;
         for (int i = 0; i < segmentCount; i++) {
             //this function tends to given a fairly even distribution
 hash = hash * 37 + segments[i].hashCode();
         }
         return hash;
     }

     /* (Intentionally not included in javadoc)
      * Returns the size of the string that will be created by toString or toOSString.
      */
     private int computeLength() {
         int length = 0;
         if (device != null)
             length += device.length();
         if ((separators & HAS_LEADING) != 0)
             length++;
         if ((separators & IS_UNC) != 0)
             length++;
         //add the segment lengths
 int max = segments.length;
         if (max > 0) {
             for (int i = 0; i < max; i++) {
                 length += segments[i].length();
             }
             //add the separator lengths
 length += max - 1;
         }
         if ((separators & HAS_TRAILING) != 0)
             length++;
         return length;
     }

     /* (Intentionally not included in javadoc)
      * Returns the number of segments in the given path
      */
     private int computeSegmentCount(String path) {
         int len = path.length();
         if (len == 0 || (len == 1 && path.charAt(0) == SEPARATOR)) {
             return 0;
         }
         int count = 1;
         int prev = -1;
         int i;
         while ((i = path.indexOf(SEPARATOR, prev + 1)) != -1) {
             if (i != prev + 1 && i != len) {
                 ++count;
             }
             prev = i;
         }
         if (path.charAt(len - 1) == SEPARATOR) {
             --count;
         }
         return count;
     }

     /**
      * Computes the segment array for the given canonicalized path.
      */
     private String [] computeSegments(String path) {
         // performance sensitive --- avoid creating garbage
 int segmentCount = computeSegmentCount(path);
         if (segmentCount == 0)
             return NO_SEGMENTS;
         String [] newSegments = new String [segmentCount];
         int len = path.length();
         // check for initial slash
 int firstPosition = (path.charAt(0) == SEPARATOR) ? 1 : 0;
         // check for UNC
 if (firstPosition == 1 && len > 1 && (path.charAt(1) == SEPARATOR))
             firstPosition = 2;
         int lastPosition = (path.charAt(len - 1) != SEPARATOR) ? len - 1 : len - 2;
         // for non-empty paths, the number of segments is
 // the number of slashes plus 1, ignoring any leading
 // and trailing slashes
 int next = firstPosition;
         for (int i = 0; i < segmentCount; i++) {
             int start = next;
             int end = path.indexOf(SEPARATOR, next);
             if (end == -1) {
                 newSegments[i] = path.substring(start, lastPosition + 1);
             } else {
                 newSegments[i] = path.substring(start, end);
             }
             next = end + 1;
         }
         return newSegments;
     }
     /**
      * Returns the platform-neutral encoding of the given segment onto
      * the given string buffer. This escapes literal colon characters with double colons.
      */
     private void encodeSegment(String string, StringBuffer buf) {
         int len = string.length();
         for (int i = 0; i < len; i++) {
             char c = string.charAt(i);
             buf.append(c);
             if (c == DEVICE_SEPARATOR)
                 buf.append(DEVICE_SEPARATOR);
         }
     }

     /* (Intentionally not included in javadoc)
      * Compares objects for equality.
      */
     public boolean equals(Object obj) {
         if (this == obj)
             return true;
         if (!(obj instanceof Path))
             return false;
         Path target = (Path) obj;
         //check leading separators and hash code
 if ((separators & HASH_MASK) != (target.separators & HASH_MASK))
             return false;
         String [] targetSegments = target.segments;
         int i = segments.length;
         //check segment count
 if (i != targetSegments.length)
             return false;
         //check segments in reverse order - later segments more likely to differ
 while (--i >= 0)
             if (!segments[i].equals(targetSegments[i]))
                 return false;
         //check device last (least likely to differ)
 return device == target.device || (device != null && device.equals(target.device));
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#getDevice
      */
     public String getDevice() {
         return device;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#getFileExtension
      */
     public String getFileExtension() {
         if (hasTrailingSeparator()) {
             return null;
         }
         String lastSegment = lastSegment();
         if (lastSegment == null) {
             return null;
         }
         int index = lastSegment.lastIndexOf('.');
         if (index == -1) {
             return null;
         }
         return lastSegment.substring(index + 1);
     }

     /* (Intentionally not included in javadoc)
      * Computes the hash code for this object.
      */
     public int hashCode() {
         return separators & HASH_MASK;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#hasTrailingSeparator2
      */
     public boolean hasTrailingSeparator() {
         return (separators & HAS_TRAILING) != 0;
     }

     /*
      * Initialize the current path with the given string.
      */
     private IPath initialize(String deviceString, String path) {
         Assert.isNotNull(path);
         this.device = deviceString;

         path = collapseSlashes(path);
         int len = path.length();

         //compute the separators array
 if (len < 2) {
             if (len == 1 && path.charAt(0) == SEPARATOR) {
                 this.separators = HAS_LEADING;
             } else {
                 this.separators = 0;
             }
         } else {
             boolean hasLeading = path.charAt(0) == SEPARATOR;
             boolean isUNC = hasLeading && path.charAt(1) == SEPARATOR;
             //UNC path of length two has no trailing separator
 boolean hasTrailing = !(isUNC && len == 2) && path.charAt(len - 1) == SEPARATOR;
             separators = hasLeading ? HAS_LEADING : 0;
             if (isUNC)
                 separators |= IS_UNC;
             if (hasTrailing)
                 separators |= HAS_TRAILING;
         }
         //compute segments and ensure canonical form
 segments = computeSegments(path);
         if (!canonicalize()) {
             //compute hash now because canonicalize didn't need to do it
 separators = (separators & ALL_SEPARATORS) | (computeHashCode() << 3);
         }
         return this;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isAbsolute
      */
     public boolean isAbsolute() {
         //it's absolute if it has a leading separator
 return (separators & HAS_LEADING) != 0;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isEmpty
      */
     public boolean isEmpty() {
         //true if no segments and no leading prefix
 return segments.length == 0 && ((separators & ALL_SEPARATORS) != HAS_LEADING);

     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isPrefixOf
      */
     public boolean isPrefixOf(IPath anotherPath) {
         if (device == null) {
             if (anotherPath.getDevice() != null) {
                 return false;
             }
         } else {
             if (!device.equalsIgnoreCase(anotherPath.getDevice())) {
                 return false;
             }
         }
         if (isEmpty() || (isRoot() && anotherPath.isAbsolute())) {
             return true;
         }
         int len = segments.length;
         if (len > anotherPath.segmentCount()) {
             return false;
         }
         for (int i = 0; i < len; i++) {
             if (!segments[i].equals(anotherPath.segment(i)))
                 return false;
         }
         return true;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isRoot
      */
     public boolean isRoot() {
         //must have no segments, a leading separator, and not be a UNC path.
 return this == ROOT || (segments.length == 0 && ((separators & ALL_SEPARATORS) == HAS_LEADING));
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isUNC
      */
     public boolean isUNC() {
         if (device != null)
             return false;
         return (separators & IS_UNC) != 0;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isValidPath(String)
      */
     public boolean isValidPath(String path) {
         Path test = new Path(path);
         for (int i = 0, max = test.segmentCount(); i < max; i++)
             if (!isValidSegment(test.segment(i)))
                 return false;
         return true;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#isValidSegment(String)
      */
     public boolean isValidSegment(String segment) {
         int size = segment.length();
         if (size == 0)
             return false;
         for (int i = 0; i < size; i++) {
             char c = segment.charAt(i);
             if (c == '/')
                 return false;
             if (WINDOWS && (c == '\\' || c == ':'))
                 return false;
         }
         return true;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#lastSegment()
      */
     public String lastSegment() {
         int len = segments.length;
         return len == 0 ? null : segments[len - 1];
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#makeAbsolute()
      */
     public IPath makeAbsolute() {
         if (isAbsolute()) {
             return this;
         }
         Path result = new Path(device, segments, separators | HAS_LEADING);
         //may need canonicalizing if it has leading ".." or "." segments
 if (result.segmentCount() > 0) {
             String first = result.segment(0);
             if (first.equals("..") || first.equals(".")) { //$NON-NLS-1$ //$NON-NLS-2$
 result.canonicalize();
             }
         }
         return result;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#makeRelative()
      */
     public IPath makeRelative() {
         if (!isAbsolute()) {
             return this;
         }
         return new Path(device, segments, separators & HAS_TRAILING);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#makeUNC(boolean)
      */
     public IPath makeUNC(boolean toUNC) {
         // if we are already in the right form then just return
 if (!(toUNC ^ isUNC()))
             return this;

         int newSeparators = this.separators;
         if (toUNC) {
             newSeparators |= HAS_LEADING | IS_UNC;
         } else {
             //mask out the UNC bit
 newSeparators &= HAS_LEADING | HAS_TRAILING;
         }
         return new Path(toUNC ? null : device, segments, newSeparators);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#matchingFirstSegments(IPath)
      */
     public int matchingFirstSegments(IPath anotherPath) {
         Assert.isNotNull(anotherPath);
         int anotherPathLen = anotherPath.segmentCount();
         int max = Math.min(segments.length, anotherPathLen);
         int count = 0;
         for (int i = 0; i < max; i++) {
             if (!segments[i].equals(anotherPath.segment(i))) {
                 return count;
             }
             count++;
         }
         return count;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#removeFileExtension()
      */
     public IPath removeFileExtension() {
         String extension = getFileExtension();
         if (extension == null || extension.equals("")) { //$NON-NLS-1$
 return this;
         }
         String lastSegment = lastSegment();
         int index = lastSegment.lastIndexOf(extension) - 1;
         return removeLastSegments(1).append(lastSegment.substring(0, index));
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#removeFirstSegments(int)
      */
     public IPath removeFirstSegments(int count) {
         if (count == 0)
             return this;
         if (count >= segments.length) {
             return new Path(device, NO_SEGMENTS, 0);
         }
         Assert.isLegal(count > 0);
         int newSize = segments.length - count;
         String [] newSegments = new String [newSize];
         System.arraycopy(this.segments, count, newSegments, 0, newSize);

         //result is always a relative path
 return new Path(device, newSegments, separators & HAS_TRAILING);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#removeLastSegments(int)
      */
     public IPath removeLastSegments(int count) {
         if (count == 0)
             return this;
         if (count >= segments.length) {
             //result will have no trailing separator
 return new Path(device, NO_SEGMENTS, separators & (HAS_LEADING | IS_UNC));
         }
         Assert.isLegal(count > 0);
         int newSize = segments.length - count;
         String [] newSegments = new String [newSize];
         System.arraycopy(this.segments, 0, newSegments, 0, newSize);
         return new Path(device, newSegments, separators);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#removeTrailingSeparator()
      */
     public IPath removeTrailingSeparator() {
         if (!hasTrailingSeparator()) {
             return this;
         }
         return new Path(device, segments, separators & (HAS_LEADING | IS_UNC));
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#segment(int)
      */
     public String segment(int index) {
         if (index >= segments.length)
             return null;
         return segments[index];
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#segmentCount()
      */
     public int segmentCount() {
         return segments.length;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#segments()
      */
     public String [] segments() {
         String [] segmentCopy = new String [segments.length];
         System.arraycopy(segments, 0, segmentCopy, 0, segments.length);
         return segmentCopy;
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#setDevice(String)
      */
     public IPath setDevice(String value) {
         if (value != null) {
             Assert.isTrue(value.indexOf(IPath.DEVICE_SEPARATOR) == (value.length() - 1), "Last character should be the device separator"); //$NON-NLS-1$
 }
         //return the receiver if the device is the same
 if (value == device || (value != null && value.equals(device)))
             return this;

         return new Path(value, segments, separators);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#toFile()
      */
     public File toFile() {
         return new File (toOSString());
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#toOSString()
      */
     public String toOSString() {
         //Note that this method is identical to toString except
 //it uses the OS file separator instead of the path separator
 int resultSize = computeLength();
         if (resultSize <= 0)
             return EMPTY_STRING;
         char FILE_SEPARATOR = File.separatorChar;
         char[] result = new char[resultSize];
         int offset = 0;
         if (device != null) {
             int size = device.length();
             device.getChars(0, size, result, offset);
             offset += size;
         }
         if ((separators & HAS_LEADING) != 0)
             result[offset++] = FILE_SEPARATOR;
         if ((separators & IS_UNC) != 0)
             result[offset++] = FILE_SEPARATOR;
         int len = segments.length - 1;
         if (len >= 0) {
             //append all but the last segment, with separators
 for (int i = 0; i < len; i++) {
                 int size = segments[i].length();
                 segments[i].getChars(0, size, result, offset);
                 offset += size;
                 result[offset++] = FILE_SEPARATOR;
             }
             //append the last segment
 int size = segments[len].length();
             segments[len].getChars(0, size, result, offset);
             offset += size;
         }
         if ((separators & HAS_TRAILING) != 0)
             result[offset++] = FILE_SEPARATOR;
         return new String (result);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#toPortableString()
      */
     public String toPortableString() {
         int resultSize = computeLength();
         if (resultSize <= 0)
             return EMPTY_STRING;
         StringBuffer result = new StringBuffer (resultSize);
         if (device != null)
             result.append(device);
         if ((separators & HAS_LEADING) != 0)
             result.append(SEPARATOR);
         if ((separators & IS_UNC) != 0)
             result.append(SEPARATOR);
         int len = segments.length;
         //append all segments with separators
 for (int i = 0; i < len; i++) {
             if (segments[i].indexOf(DEVICE_SEPARATOR) >= 0)
                 encodeSegment(segments[i], result);
             else
                 result.append(segments[i]);
             if (i < len-1 || (separators & HAS_TRAILING) != 0)
                 result.append(SEPARATOR);
         }
         return result.toString();
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#toString()
      */
     public String toString() {
         int resultSize = computeLength();
         if (resultSize <= 0)
             return EMPTY_STRING;
         char[] result = new char[resultSize];
         int offset = 0;
         if (device != null) {
             int size = device.length();
             device.getChars(0, size, result, offset);
             offset += size;
         }
         if ((separators & HAS_LEADING) != 0)
             result[offset++] = SEPARATOR;
         if ((separators & IS_UNC) != 0)
             result[offset++] = SEPARATOR;
         int len = segments.length - 1;
         if (len >= 0) {
             //append all but the last segment, with separators
 for (int i = 0; i < len; i++) {
                 int size = segments[i].length();
                 segments[i].getChars(0, size, result, offset);
                 offset += size;
                 result[offset++] = SEPARATOR;
             }
             //append the last segment
 int size = segments[len].length();
             segments[len].getChars(0, size, result, offset);
             offset += size;
         }
         if ((separators & HAS_TRAILING) != 0)
             result[offset++] = SEPARATOR;
         return new String (result);
     }

     /* (Intentionally not included in javadoc)
      * @see IPath#uptoSegment(int)
      */
     public IPath uptoSegment(int count) {
         if (count == 0)
             return new Path(device, NO_SEGMENTS, separators & (HAS_LEADING | IS_UNC));
         if (count >= segments.length)
             return this;
         Assert.isTrue(count > 0, "Invalid parameter to Path.uptoSegment"); //$NON-NLS-1$
 String [] newSegments = new String [count];
         System.arraycopy(segments, 0, newSegments, 0, count);
         return new Path(device, newSegments, separators);
     }
 }
